#
# GameProxy class - encapsulates all business logic for the FGS proxy.
#

from twisted.internet.protocol import ServerFactory
from twisted.internet.defer import succeed
from twisted.application.service import MultiService
from twisted.python import log

from fgs.proxy import error

def make_uuid():
    from random import randint
    return ''.join(["%x" % randint(0,255) for _ in range(16)])

class DelayedCommitDict(object):
    def __init__(self):
        """Initialize a delayed commit dictionary."""
        from sets import Set

        self._data = dict()
        self._pending = Set()

    def __getitem__(self, key):
        return self._data[key]

    def __setitem__(self, key, value):
        self._data[key] = value
        self._pending.add(key)

    def __delitem__(self, key):
        # Deletion is implemented as setting the "" value, because
        # that's how the fgs-lib reports deletions anyway, and the
        # upstream backend treats empty strings as deletions too. So
        # there is no real use to enforce the distinction in the
        # intermediate level.
        self.__setitem__(key, "")

    def collect_changes(self):
        """This method can be overriden to provide extra data
        collection for do_commit.  Return None to not proceed with the
        commit."""
        update_data = [k for k in self._pending]

        if len(update_data) == 0:
            return None
        else:
            return [update_data]

    def do_commit(self, *args):
        """Override in derived classes to do the actual commit"""

    def clear_changes(self):
        """Override this method to provide extra "data cleansing", if
        you need any."""
        self._pending.clear()

    def commit(self):
        data = self.collect_changes()

        if data != None:
            self.do_commit(*data)
            self.clear_changes()

    def __repr__(self):
        return self._data.__repr__()

class PlayerMatchDict(DelayedCommitDict):
    def __init__(self, player, match, backend):
        import logging
        DelayedCommitDict.__init__(self)
        self._backend = backend
        self.player = player
        self.match = match
        self._log = logging.getLogger('proxy.playermatch')


class MatchDict(DelayedCommitDict):
    def __init__(self, backend):
        import logging
        DelayedCommitDict.__init__(self)
        self._backend = backend
        self.uuid = make_uuid()
        self.players = {}
        self._log = logging.getLogger('proxy.match')

    def collect_changes(self):
        match_updates = DelayedCommitDict.collect_changes(self)
        player_updates = {}
        for player in self.players.values():
            data = player.collect_changes()
            if data != None:
                player_updates[player] = data[0]
        if match_updates == None and len(player_updates) == 0:
            return None
        else:
            match_updates = match_updates or [[]]
            return match_updates + [player_updates]

    def do_commit(self, match_updates, player_updates):
        self._log.debug("Committing match and player/match data for match %s",
                       self.uuid)

        self._backend.match_update(self, match_updates, player_updates)

    def clear_changes(self):
        DelayedCommitDict.clear_changes(self)
        for player in self.players.values():
            player.clear_changes()

    def add_player(self, player):
        p = PlayerMatchDict(player, self, self._backend)
        self.players[player.uuid] = p
        self._log.debug("Adding player %s to match %s", player.uuid, self.uuid)
        self._backend.match_add_player(self, player)

    def del_player(self, player):
        self._backend.match_remove_player(self, player)
        self.commit()
        del self.players[player.uuid]

    def discard_player(self, player):
        if self.players.has_key(player.uuid):
            self.del_player(player)

    def clear_players(self):
        for player in self.players.values():
            self.del_player(player.player)
        self.players.clear()

class PlayerDict(DelayedCommitDict):
    def __init__(self, backend, login=None):
        import logging
        DelayedCommitDict.__init__(self)
        self._backend = backend
        self.login = login
        self.uuid = make_uuid()
        self._log = logging.getLogger('proxy.player')

    def do_commit(self, update_data):
        self._log.debug("Committing player information for player %s",
                        self.uuid)
        self._backend.player_update(self, update_data)

class GameServer(object):

    def __init__(self, backend, game_name, server_version, listen_port):
        import logging
        self._backend = backend
        self._log = logging.getLogger('proxy.gameserver')

        self.game_name = game_name
        self.version = server_version
        self.port = listen_port
        self.uuid = make_uuid()
        self.players = {}
        self.matches = {}

        self.sid = None

    def get_config(self):
        return ()

    def add_player(self, login=None, token=None, nonce=None):
        """Add the player to the game server.  Return a deferred
        returning the new player object."""

        # If we're adding an anonymous player, things are easy. Just
        # return a new anonymous player object.
        if login == None:
            p = PlayerDict(self._backend)
            self.players[p.uuid] = p
            return succeed(p)

        # If we're trying to add an authenticated player, we need to
        # run some background checks first, and return an
        # authenticated player object.
        else:

            # First callback level. If the auth token validation
            # succeeds, then request addition of the player and chain
            # the resulting Deferred.
            def auth_checked(res):
                # Callbacks for the addition request.
                def add_success(player):
                    self._log.info("New logged in player %s on %s",
                                   login, self)
                    self.players[player.uuid] = player
                    return player
                def add_error(e):
                    self._log.error("Error during addition of player"
                                    "%s to %s: %s",
                                    login, self, e.value)
                    raise e

                if res == False:
                    raise error.FgsProxyUnauthorizedError()

                # Authentication successful, now request addition.
                p = PlayerDict(self._backend, login)
                d = self._backend.user_join(self, p, login, token, nonce)
                d.addCallbacks(add_success, add_error)
                return d
            def auth_error(e):
                self._log.error("Error during player authentication: %s",
                                e.value)
                raise e

            d = self._backend.user_verify(login, token, nonce)
            d.addCallbacks(auth_checked, auth_error)

            # Return the authentication Deferred. The caller will see
            # this fired either with a created Player object, or with
            # an error.
            return d

    def add_match(self):
        match = MatchDict(self._backend)
        self.matches[match.uuid] = match
        self._backend.server_new_match(self, match)
        return match

    def del_match(self, match_uuid):
        self.matches[match_uuid].commit()
        self.matches[match_uuid].clear_players()
        self._backend.match_end(self, self.matches[match_uuid])
        del self.matches[match_uuid]

    def del_player(self, player_uuid):
        for match in self.matches.values():
            match.discard_player(self.players[player_uuid])
        self.players[player_uuid].commit()
        self._backend.user_leave(self, self.players[player_uuid])
        del self.players[player_uuid]

class GameProxy(MultiService):
    def __init__(self, config, backend):
        import logging
        MultiService.__init__(self)
        self.config = config
        self.servers = []
        self._backend = backend
        self._log = logging.getLogger('proxy')

    def startService(self):
        MultiService.startService(self)
        self._backend.register()

    def stopService(self):
        from twisted.internet import reactor, defer

        def handle_success(val, reactor_defer):
            reactor.callLater(0, reactor_defer.callback, val)
        def handle_failure(fail, reactor_defer):
            reactor.callLater(0, reactor_defer.errback, fail)

        MultiService.stopService(self)

        # The reactor must not continue shutdown within the backend's
        # callback, as it might trigger some double-firing of the
        # Deferred.
        #
        # So, we create a second Deferred for the reactor, and
        # schedule it to fire when the backend deferred completes.
        # That way, the backend Deferred has fully completed before
        # the shutdown resumes.
        reactor_defer = defer.Deferred()
        backend_defer = self._backend.unregister()
        backend_defer.addCallbacks(handle_success,
                                   handle_failure,
                                   callbackArgs=[reactor_defer],
                                   errbackArgs=[reactor_defer])
        return reactor_defer

    def new_gameserver(self, game_name, server_version, listen_port):
        """Add a new gameserver to the proxy.  Return a deferred
        returning the registered gameserver."""
        def addition_success(gameserver):
            self._log.info("New %s server v%s, running on port %s",
                           game_name, server_version, listen_port)
            self.servers.append(gameserver)
            return gameserver
        def addition_error(e):
            self._log.error("Failed to add new gameserver: %s", e.value)
            raise e

        g = GameServer(self._backend, game_name, server_version, listen_port)
        res = self._backend.server_register(g)
        res.addCallbacks(addition_success, addition_error)

        return res

    def del_gameserver(self, gameserver):
        self._log.info("Removing gameserver %s", gameserver)
        self.servers.remove(gameserver)
        self._backend.server_unregister(gameserver)
